/*
 * 代号：凤凰
 * http://www.jphenix.org
 * 2018年6月25日
 * V4.0
 */
package com.jphenix.driver.guard;

import com.jphenix.driver.cluster.ClusterFilter;
import com.jphenix.driver.cluster.IClusterTrigger;
import com.jphenix.driver.cluster.ServerInfoVO;
import com.jphenix.driver.log.xlogc.XLogFilter;
import com.jphenix.driver.nodehandler.FNodeHandler;
import com.jphenix.kernel.baseobject.instanceb.ABase;
import com.jphenix.servlet.filter.FilterExplorer;
import com.jphenix.servlet.multipart.instancea.DownloadFile;
import com.jphenix.share.lang.SBoolean;
import com.jphenix.share.lang.SListMap;
import com.jphenix.share.lang.SLong;
import com.jphenix.share.tools.FileCopyTools;
import com.jphenix.share.tools.MD5;
import com.jphenix.share.util.BaseUtil;
import com.jphenix.share.util.SFilesUtil;
import com.jphenix.standard.docs.BeanInfo;
import com.jphenix.standard.docs.ClassInfo;
import com.jphenix.standard.docs.Register;
import com.jphenix.standard.docs.Running;
import com.jphenix.standard.servlet.IFilter;
import com.jphenix.standard.servlet.IRequest;
import com.jphenix.standard.servlet.IResponse;
import com.jphenix.standard.viewhandler.IViewHandler;

import javax.servlet.FilterConfig;
import java.io.File;
import java.io.FilenameFilter;
import java.io.InputStream;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

/**
 * 文件分发过滤器
 * com.jphenix.driver.guard.FileGuardFilter
 * 
 * 业务逻辑
 * 
 * 1. 初始化配置信息 、启动监控文件线程LocalFileMonitorThread
 * 2. 文件监控线程，定时检查本地文件是否发生变化，以及目前联机的集群服务器总数量是否与已同步时间戳对照容器serverMap中的服务器数量是否一致
 * 3. 如果文件发生变化，或者活动服务器数量发生变化，则将本地文件信息推送到目标服务器中。
 * 4. 如果从已同步时间戳对照容器中没有获取到目标服务器的时间戳，则将本地全量文件信息推送给目标服务器。
 * 5. 如果从已同步时间戳对照容器中获取到上一次的时间戳，并且跟本次同步时间戳不一致，则只推送后期发生变化的文件信息到目标服务器
 * 
 * 
 * 2018-06-29 支持指定服务器为不同群组提供文件同步共享功能
 * 2018-07-26 不能单纯以文件更新时间来判断文件是否发生变动，改为MD5值判断
 * 2018-07-27 增加了并发控制，以及统计信息变量
 * 2018-07-30 增加了写入日志功能
 * 2018-07-31 在广播推送文件信息后，除去已删除的文件信息，并且复位发生变化的文件信息
 *            增加了更新关键文件后，是否准备重启的方法
 * 2019-01-08 弃用了invalidServer，请看变量ServerInfoVO.invalidServer声明处的注释
 * 2019-05-16 halt后，有些连接的Tcp Socket 会出现TIME_WAIT 和 CLOSE_WAIT，所以遇到这种情况时，在配置文件中增加system_no_halt参数
 * 2019-06-15 按照IFilter增加了过滤器初始化方法
 * 2020-03-13 修改了从URL中获取请求参数
 * 2020-03-27 在执行终止系统之前，先输出一条日志。有时候执行终止时，卡在那里
 * 2020-10-04 增加了排除不需要同步的文件处理
 * 2020-10-05 修复了删除文件后无法同步到通群组其它服务器的问题
 * 
 * @author MBG
 * 2018年6月25日
 */
@ClassInfo({"2020-10-05 20:54","文件分发过滤器"})
@BeanInfo({"fileguardfilter"})
@Register({"filtervector,clusterfilter"})
@Running({"1","","","destroy"})
public class FileGuardFilter extends ABase implements IFilter,IClusterTrigger {

	/**
	 * 过滤器响应请求路径
	 */
	public final static String FILE_GUARD_ACTION = "/_jg_.ha";
	
	private GuardConfigVO                   config               = new GuardConfigVO(this);    //配置信息类
	private Map<String,List<FileVO>>        fileListMap          = null;                       //key:集群分组名 value:文件对象序列
	private Map<String,FileVO>              fileMap              = null;                       //文件信息容器  key：文件路径MD5值 value：FileVO
	private ClusterFilter                   cf                   = null;                       //集群核心类
	private Map<String,Long>                serverMap            = new HashMap<String,Long>(); //集群分组内服务器信息序列 key:服务器名 value:发送文件同步时间错
	private long                            lastSyncTS           = 0;                          //最后一次同步时间戳
	private LocalFileMonitorThread          lfThread             = null;                       //本地文件监控线程
	private RebootThread                    rebootThread         = null;                       //延时重启线程
	public  boolean                         hasChecking          = false;                      //是否在检测本地文件是否发生变化
	public  boolean                         hasSync              = false;                      //是否正在同步中
	public  long                            checkCount           = 0;                          //检测次数
	public  long                            syncCount            = 0;                          //被同步次数
	public  long                            sendFileCount        = 0;                          //推送文件次数
	public  long                            resFileCount         = 0;                          //下载文件次数
	public  long                            delFileCount         = 0;                          //删除文件次数
	public  long                            reduceFileCount      = 0;                          //发现本地文件减少次数
	public  String                          lastCheckTime        = "";                         //上次监控检查本地文件时间
	public  String                          lastSyncTime         = "";                         //上次被同步时间
	public  final String                    LOG_KEY              = "file_guard";               //日志文件头
	
	/**
	 * 延时重启线程
	 * @author MBG
	 * 2018年6月27日
	 */
	private class RebootThread extends Thread {
		
		public long    outTime = 0;
		
		/**
		 * 构造函数
		 * @author MBG
		 */
		public RebootThread() {
			super("FileGuardFilter-RebootThread");
		}
		
		/**
		 * 覆盖方法
		 */
		@Override
        public void run() {
			while(true) {
				if(outTime>0 && outTime<System.currentTimeMillis()) {
					warning("!!!!!!!!!!!!!!!!!!!!FileGuardFilter Begin Reboot!!!!!!!!!!!!!!!!!!!!");
				    //有时候Tomcat或者其他Servlet容器会执行这个该死的方法，导致
				    //应用半死不活成为僵尸应用，不如在这里快刀斩乱麻直接终止java
				    //进程，同行在配置启动应用脚本时，会配置成死循环，终止java进程
				    //后会自动再次运行改进程
				    //注意，不能使用 System.exit(1); 因为如果程序
				    //在别的地方用了Runtime.getRuntime().addShutdownHook(Thread); 
				    //而传入的线程发生了阻塞，就不会停止进程了
				    //注意：halt后，有些连接的Tcp Socket 会出现TIME_WAIT 和 CLOSE_WAIT
				    //，所以遇到这种情况时，在配置文件中增加<system_no_halt>true</system_no_halt>
					if(boo(p("system_no_halt"))) {
						System.err.print("******System Begin Exit******");
						System.exit(1);
					}else {
						System.err.print("******System Begin Halt******");
						Runtime.getRuntime().halt(1);
					}
				    //注意在此销毁类工厂后，就再也启动不了了
				    //RootBeanFactory.destroyBeanFactory();
				}
				try {
					Thread.sleep(1000);
				}catch(Exception e) {}
			}
		}
		
		/**
		 * 准备重启
		 * 2018年6月27日
		 * @author MBG
		 */
		public void ready() {
			//一分钟后重启
			outTime = System.currentTimeMillis()+(60*1000);
		}
	}
	
	/**
	 * 本地文件监控线程
	 * @author MBG
	 * 2018年6月26日
	 */
	private class LocalFileMonitorThread extends Thread {
		
		private boolean isRun = true; //是否允许运行
		
		/**
		 * 构造函数
		 * @author MBG
		 */
		public LocalFileMonitorThread() {
			super("FileGuardFilter-LocalFileMonitorThread");
		}
		
		/**
		 * 覆盖方法
		 */
		@Override
        public void run() {
			
			while(isRun) {
				int serverCount = 0; //目前在线的服务器数量
				//获取从配置文件中提取出的监控文件分组名序列
				List<String> groups = BaseUtil.getMapKeyList(fileListMap);
				List<ServerInfoVO> siVOList; //服务器信息序列
				for(String group:groups) {
					//获取服务器信息序列
					siVOList = cf.getServerInfoList(group);
					for(ServerInfoVO siVO:siVOList) {
						//已弃用，请看变量ServerInfoVO.invalidServer声明处的注释
						//if(siVO.local || siVO.disabled || siVO.invalidServer || !siVO.validSend) {
						if(siVO.local || siVO.disabled || !siVO.validSend) {
							continue;
						}
						serverCount++;
					}
				}
				if(refreshFileInfo() || serverCount!=serverMap.size()) {
					//文件发生变化，或初始化，或者服务器数量发生了变化
					
					//将本地文件信息广播给群组其它服务器
					lastSyncTS = System.currentTimeMillis();
					broadcastInfo();
				}
				try {
					Thread.sleep(30000);
				}catch(Exception e) {}
			}
			isRun = true;
		}
		
		/**
		 * 覆盖方法
		 */
		@Override
        public void destroy() {
			isRun = false;
		}
	}
	
	/**
	 * 构造函数
	 * @author MBG
	 */
	public FileGuardFilter() {
		super();
	}

	/**
	 * 过滤器索引（有小到大优先）
	 */
	@Override
	public int getIndex() {
		return 10;
	}

	/**
	 * 响应动作扩展名
	 */
	@Override
	public String getFilterActionExtName() {
		return "ha";
	}

	/**
	 * 响应请求
	 */
	@Override
	public boolean doFilter(IRequest req, IResponse resp) throws Exception {
		if(config.disasbled) {
			return false;
		}
		if(!req.getServletPath().equals(FILE_GUARD_ACTION)) {
			return false;
		}
		if(!cf.allow(req)) {
			return true;
		}
		if("1".equals(req.getUrlParameter("sync"))) {
			//接收到同群组其它服务器发来的文件信息
			IViewHandler resVh = FNodeHandler.getNodeHandler(req.getInputStream());
			
			syncFileInfo(resVh); //执行同步
		}else if("1".equals(req.getUrlParameter("download"))) {
			//文件主键
			String fileId = req.getUrlParameter("id");
			if(fileId==null ||fileId.length()<1) {
				return true;
			}
			//获取对应的文件对象
			FileVO fVO = fileMap.get(fileId);
			if(fVO==null) {
				return true;
			}
			//累加发送次数
			if(++sendFileCount>=Long.MAX_VALUE) {sendFileCount=0;}
			
			//构建文件下载类
			DownloadFile df = new DownloadFile(resp,null);
			
			log(LOG_KEY,"[local] [send] remote_ip:["+req.getRemoteAddr()+"] "+fVO.path);
			
			//执行文件下载处理
			df.downloadFile(filesUtil.getAllFilePath(fVO.basePath+fVO.path));
		}
		return true;
	}

	/**
	 * 终止服务
	 */
	@SuppressWarnings("deprecation")
	protected void destroy() {
		try {
			lfThread.stop();
		}catch(Exception e) {}
		lfThread = null;
		
		try {
			rebootThread.stop();
		}catch(Exception e) {}
		rebootThread = null;
	}
	
	/**
	 * 执行初始化
	 * @param fe         过滤器管理类
	 * @param config     Servlet配置信息类
	 * @throws Exception 异常（如果初始化发生异常，则放弃不再使用）
	 * 2019年6月15日
	 * @author MBG
	 */
	@Override
	public void init(FilterExplorer fe, FilterConfig config) throws Exception {}
	
	/**
	 * 将本机文件信息广播到集群分组内其它服务器
	 * 2018年6月26日
	 * @author MBG
	 */
    public synchronized void broadcastInfo() {
      // 获取本地监控文件预设的分组名序列
      List<String> groupList = BaseUtil.getMapKeyList(fileListMap);
      List<ServerInfoVO> infoList; // 指定群组的集群信息
      IViewHandler allData = null; // 完整的数据
      IViewHandler changeData = null; // 本地发生变化的数据
      Long sTs; // 上次同步时间戳
      String url = FILE_GUARD_ACTION + "?sync=1"; // 调用动作
      Map<String, IViewHandler> allDataMap; // 即将发送的全量数据缓存
      Map<String, IViewHandler> dataMap; // 即将发送的增量数据缓存
      for (String group : groupList) {
        if (group == null || group.length() < 1) {
          continue;
        }
        // 获取当前服务器集群信息
        infoList   = cf.getServerInfoList(group);
        allDataMap = new HashMap<String, IViewHandler>(); // 即将发送的全量数据缓存
        dataMap    = new HashMap<String, IViewHandler>(); // 即将发送的增量数据缓存
        for (ServerInfoVO siVO : infoList) {
          if (siVO.local) {
            continue;
          }
          // 已弃用，请看变量ServerInfoVO.invalidServer声明处的注释
          // if(siVO.disabled || siVO.invalidServer || !siVO.validSend) {
          if (siVO.disabled || !siVO.validSend) {
            serverMap.remove(siVO.name);
            continue;
          }
          sTs = serverMap.get(siVO.name);
          if (sTs == null) {
            // 发送完整数据
            allData = allDataMap.get(siVO.group);
            if (allData == null) {
              allData = getFileInfo(siVO.group, false);
              allDataMap.put(siVO.group, allData);
            }
            if (allData != null) {
              // 执行发送
              try {
                cf.callAction(siVO.name, url, allData);
              } catch (Exception e) {
                serverMap.remove(siVO.name);
                e.printStackTrace();
                continue;
              }
            }
          } else if (sTs != lastSyncTS) {
            changeData = dataMap.get(siVO.group);
            if (changeData == null) {
              changeData = getFileInfo(siVO.group, true);
              dataMap.put(siVO.group, changeData);
            }
            if (changeData != null) {
              // 执行发送
              try {
                cf.callAction(siVO.name, url, changeData);
              } catch (Exception e) {
                serverMap.remove(siVO.name);
                e.printStackTrace();
                continue;
              }
            }
          }
          serverMap.put(siVO.name, lastSyncTS);
        }
      }
      // 广播后除去已删除的文件和重置发生变化标识
      FileVO ele; // 文件元素
      List<String> idList = BaseUtil.getMapKeyList(fileMap); // 获取文件信息主键序列
      List<FileVO> fileList; // 指定群组中的文件序列
      for (String id : idList) {
        ele = fileMap.get(id);
        if (ele == null || !ele.changed) {
          continue;
        }
        if (ele.deleted) {
          fileMap.remove(id);
          fileList = fileListMap.get(ele.group);
          if (fileList != null) {
            fileList.remove(ele);
          }
          continue;
        }
        ele.changed = false;
      }
    }
	
    /**
     * 获取本机文件信息（用于同步到集群服务器）
     * 注意：获取发生变化的信息后，发生变化的文件信息会被标记没有变化
     * @param onlyChanged 是否只生成发生变化的
     * @return 文件信息数据 2018年6月26日
     * @author MBG
     */
    private IViewHandler getFileInfo(String group, boolean onlyChanged) {
      // 构建返回值
      IViewHandler reVh = FNodeHandler.newNodeHandler();
      reVh.setXmlStyle(true);
      reVh.nn("root").a("server", cf.name()).a("group", cf.group());
      boolean deleted; // 文件是否需要删除
      boolean hasData = false; // 是否不存在文件数据
      // 获取对应集群分组的文件对象序列
      List<FileVO> fileList = fileListMap.get(group);
      if (fileList != null) {
        for (FileVO fVO : fileList) {
          if(fVO.ignore){
            continue;
          }
          if (onlyChanged) {
            if (fVO.changed) {
              fVO.changed = false;
            } else {
              continue;
            }
          }
          if (fVO.deleted) {
            deleted = fVO.deleted;
          } else {
            deleted = false;
          }
          reVh.ac("file").a("id", fVO.id).a("group", fVO.group).a("deleted", (deleted ? "1" : "0"))
              .a("base_path", fVO.basePath).a("path", fVO.path).a("md5", fVO.md5 == null ? "" : fVO.md5)
              .a("dir", fVO.isDir ? "1" : "0").a("length", String.valueOf(fVO.length))
              .a("time", String.valueOf(fVO.time));
          hasData = true;
        }
      }
      if (hasData) {
        return reVh;
      }
      return null;
    }
	
	
	/**
	 * 刷新本地作业文件信息
	 * @return 是否存在发生变化的文件（包含初始化）
	 * 2018年6月26日
	 * @author MBG
	 */
	public boolean refreshFileInfo() {
		lastCheckTime  = ts();  //记录本地检测时间
		if(++checkCount>=Long.MAX_VALUE) {checkCount=0;} //累加检测次数
		boolean res    = false; //构建返回值
		boolean isInit = false; //是否为初始化
		if(fileListMap==null) {
			fileListMap = new HashMap<String,List<FileVO>>();
			isInit      = true;
		}
		if(fileMap==null) {
			fileMap = new HashMap<String,FileVO>();
		}
		String localGroup = cf.group(); //当前服务器所属分组名
		//已有文件主键序列
		List<String> fileIdList = BaseUtil.getMapKeyList(fileMap);
		boolean      changed    = false; //文件是否发生了变化
		File         fileEle;            //文件对象
		List<FileVO> fileList;           //指定分组中的文件序列
		String       group;              //路径对应的分组名
		String       path;               //路径元素
		for(int i=0;i<config.pathList.size();i++) {
			path  = config.pathList.get(i);
			group = config.groupList.get(i);
			if(group.length()<1) {
				group = localGroup;
			}
			fileEle = new File(filesUtil.getAllFilePath(path));
			if(!fileEle.exists() || !fileEle.isDirectory()) {
				continue;
			}
			fileList = fileListMap.get(group);
			if(fileList==null) {
				fileList = new ArrayList<FileVO>();
				fileListMap.put(group,fileList);
			}
			//执行搜索文件
			changed = search(fileEle,BaseUtil.swapString(fileEle.getPath(),"\\","/"),"",path,group,fileList,fileMap,fileIdList,isInit);
			if(!res) {
				res = changed;
			}
		}
		if(fileIdList.size()>0) {
			//存在被删除的文件
			FileVO fVO; //处理后的文件对象
			for(String id:fileIdList) {
				fVO = fileMap.get(id);
				if(fVO!=null && !fVO.deleted) {
					fVO.changed = true;
					fVO.deleted = true;
          log(LOG_KEY,"[local] [deleted] id:["+id+"] md5:["+fVO.md5+"] path:["+fVO.path+"] hash:["+fVO.hashCode()+"]");
					if(++reduceFileCount>=Long.MAX_VALUE) {reduceFileCount=0;}
					changed = true;
				}
      }
      res = changed;
		}
		return res;
	}
	
	/**
	 * 搜索指定文件夹中的文件信息
	 * @param base           需要搜索的路径对象
	 * @param checkBasePath  待监控的跟文件夹
	 * @param subPath        相对根路径
	 * @param pathKey        需要设置到文件对象中的值
	 * @param group          文件所属群组名
	 * @param fList          文件对象序列
	 * @param fMap           文件对象容器
	 * @param idList         已有的文件信息主键序列（用于鉴别已删除的文件）
	 * @param isInit         是否为初始化时执行
	 * @return               文件信息是否发生变化
	 * 2018年6月27日
	 * @author MBG
	 */
    private boolean search(File base, String checkBasePath, String subPath, String pathKey, String group,
        List<FileVO> fList, Map<String, FileVO> fMap, List<String> idList, boolean isInit) {
      boolean changed = false; // 文件是否发生了变化
      // 获得当前路径下，全部
      String[] paths = base.list(new FilenameFilter() {
        // 文件筛选类
        @Override
        public boolean accept(File dirPathFile, String lastNameStr) {
          return true;
        }
      });
      List<String> excludeList = config.excludeMap.get(pathKey); //排除文件配置信息序列
      // 判断是否存在文件或文件夹
      if (paths != null && paths.length > 0) {
        File    file;   // 查询到的文件或文件夹
        String  path;   // 当前文件路径
        String  id;     // 文件信息主键
        FileVO  fVO;    // 文件信息对象
        boolean isDir;  // 是否为文件夹
        long    length; // 文件大小
        long    time;   // 最后修改时间
        for (String pathEle : paths) {
          path = subPath + "/" + pathEle;
          file = config.validFile(checkBasePath, path);
          if (file == null) {
            continue;
          }
          path  = BaseUtil.swap(path,"\\","/");
          isDir = file.isDirectory();
          time  = file.lastModified();
          id    = MD5.getMD5Value(path);
          fVO   = fMap.get(id);
          if (isDir) {
            length = 0;
          } else {
            length = file.length();
          }
          if (fVO == null) {
            fVO    = new FileVO();
            fVO.id         = id;
            fVO.group      = group;
            fVO.basePath   = pathKey;
            fVO.path       = path;
            fVO.md5        = file.isFile() ? MD5.md5(file) : "";
            fVO.length     = length;
            fVO.time       = time;
            fVO.isDir      = isDir;
            fVO.needReboot = config.needReboot(file);
            fVO.ignore     = checkExclude(path,excludeList);
            fList.add(fVO);
            fileMap.put(id, fVO);
            if (isInit) {
              fVO.changed = false;// 初始化时，默认文件都没有变化
            } else {
              fVO.changed = true; // 初始化时，默认文件都没有变化
              log(LOG_KEY, "[local] [new] id:["+fVO.id+"] [" + fVO.md5 + "] path:[" + path+"] hash:["+fVO.hashCode()+"]");
              changed = true;
            }
          } else {
            idList.remove(fVO.id);
            if (!isDir && time != fVO.time && !fVO.ignore) {
              fVO.length  = length;
              fVO.time    = time;
              fVO.md5     = MD5.md5(file);
              fVO.changed = true;
              fVO.deleted = false;
              changed     = true;
              log(LOG_KEY, "[local] [change] id:["+fVO.id+"] [" + fVO.md5 + "] path:[" + path+"] hash:["+fVO.hashCode()+"]");
            }
          }
          if (isDir) {
            // 如果是文件夹，并且搜索子文件夹，则递归调用方法
            boolean res = search(file, checkBasePath, path, pathKey, group, fList, fMap, idList, isInit);
            if (!changed) {
              changed = res;
            }
          }
        }
      }
      return changed;
    }
  
    /**
     * 检测指定相对路径是否为排除在外不做同步
     * @param subPath       待检测路径
     * @param excludeList   排除信息序列
     * @return              是否忽略不做同步
     */
    private boolean checkExclude(String subPath,List<String> excludeList){
      if(excludeList==null || subPath==null || subPath.length()<1){
        return false;
      }
      int point;  //分隔符
      for(String cPath:excludeList){
        if(cPath==null || cPath.length()<1){
          continue;
        }
        cPath = cPath.trim();
        if(cPath.startsWith("*")){
          cPath = cPath.substring(1);
          if(subPath.endsWith(cPath)){
            return true;
          }
        }else if(cPath.endsWith("*")){
          cPath = cPath.substring(0,cPath.length()-1);
          if(subPath.startsWith(cPath)){
            return true;
          }
        }else{
          point = cPath.indexOf("*");
          if(point<0){
            if(subPath.equals(cPath)){
              //不能默认开头匹配，因为比如：不同步/WEB-INF/lib/jphenix_sdk.jar 但是需要同步 /WEB-INF/lib/jphenix_sdk.jar.new 
              //如果开头匹配，导致.new文件也无法同步
              return true;
            }
          }else{
            if(subPath.startsWith(cPath.substring(0,point)) && subPath.endsWith(cPath.substring(point+1))){
              return true;
            }
          }
        }
      }
      return false;
    }
	
	/**
	 * 执行同步比对文件信息
	 * @param vh 文件信息对象
	 * 2018年6月26日
	 * @author MBG
	 */
	public void syncFileInfo(IViewHandler vh) {
		if(hasSync) {
			return;
		}
		vh = vh.fnn("root");
		String objServerName = vh.a("server"); //目标服务器名
		String objGroup      = vh.a("group");  //目标服务器所在群组
		if(objServerName.length()<1 
				|| !(fileListMap.containsKey(objGroup) 
						|| objGroup.equals(cf.group()))) {
			return;
		}
		if(!config.isShareMode && !config.masterServerName.equals(objServerName)) {
			//如果不是共享模式，并且对方服务器也不是主服务器，则忽略更新
			return;
		}
		lastSyncTime = ts(); //记录本次被检测时间
		if(++syncCount>=Long.MAX_VALUE) {syncCount=0;} //累加被同步次数
		hasSync = true;
		//是否需要将本机文件信息推送给目标服务器
		boolean needSend = false;
		//发现需要从对方获取的文件主键序列
		SListMap<IViewHandler> dFileList = new SListMap<IViewHandler>();
		//需要删除的文件对象序列
		List<FileVO> deleteVOList = new ArrayList<FileVO>();
		//文件信息对象序列
		List<IViewHandler> fileVhList = vh.cnn("file");
		String  fileId;  //文件信息主键
		FileVO  fVO;     //本机文件信息序列
		String  md5;     //文件md5值
		long    time;    //文件更新时间
		for(IViewHandler cvh:fileVhList) {
			fileId = cvh.a("id");
			if(fileId.length()<1) {
				continue;
			}
      fVO = fileMap.get(fileId);
			if(SBoolean.valueOf(cvh.a("deleted"))) {
				if(fVO!=null && !fVO.ignore) {
					//删除该文件或文件夹
					deleteVOList.add(fVO);
				}
				continue;
			}
			if(fVO==null) {
				dFileList.put(fileId,cvh);
				continue;
      }
      if(fVO.ignore){
        //该文件不做处理
        continue;
      }
			time = SLong.valueOf(cvh.a("time"));
			md5 = cvh.a("md5");
			if(!md5.equals(fVO.md5)) {
				if(fVO.time<time || !config.isShareMode) {
					dFileList.put(fileId,cvh);
					continue;
				}else {
					needSend = true;
				}
			}
		}
		if(needSend) {
			//如果本机文件比目标文件新，则发送本机文件信息到目标服务器
			try {
				//获取即将发送的数据
				IViewHandler data = getFileInfo(objGroup,false);
				if(data!=null) {
					cf.callAction(objServerName,FILE_GUARD_ACTION+"?sync=1",data);
				}
			}catch(Exception e) {
				e.printStackTrace();
			}
		}
		if(deleteVOList.size()>0) {
			delete(objServerName,objGroup,deleteVOList); //执行删除文件
		}
		//从目标服务器下载文件到本地
		if(dFileList.size()>0) {
			download(objServerName,objGroup,dFileList);
		}
		hasSync = false;
	}
	
	/**
	 * 删除指定文件
	 * @param serverName 来源服务器名
	 * @param groupName  来源服务器分组名
	 * @param fList      需要删除的文件对象序列
	 * 2018年6月27日
	 * @author MBG
	 */
	private void delete(String serverName,String groupName,List<FileVO> fList) {
		//先删除文件
		for(FileVO ele:fList) {
			if(ele.isDir || ele.ignore) {
				continue;
			}
			//累计删除文件次数
			if(++delFileCount>=Long.MAX_VALUE) {delFileCount=0;}
			(new File(filesUtil.getAllFilePath(ele.basePath+ele.path))).delete();
		}
		//再删除文件夹
		for(FileVO ele:fList) {
			if(!ele.isDir || ele.ignore) {
				continue;
			}
			try {
				SFilesUtil.deletePath(filesUtil.getAllFilePath(ele.basePath+ele.path));
			}catch(Exception e) {}
			
			log("[remote] [delete} group:["+groupName+"] server:["+serverName+"] id:["+ele.id+"] ["+ele.md5+"] path:["+ele.path+"] hash:["+ele.hashCode()+"]");
			ele.deleted = true;
			fileMap.remove(ele.id);
			//指定分组中的文件序列
			List<FileVO> groupFileList = fileListMap.get(groupName);
			if(groupFileList!=null) {
				groupFileList.remove(ele);
			}
		}
	}
	
	/**
	 * 从目标服务器下载文件覆盖本地
	 * @param serverName  目标服务器名
	 * @param groupName   目标服务器所属分组名
	 * @param dFileList   文件主键序列
	 * 2018年6月26日
	 * @author MBG
	 */
	private void download(String serverName,String groupName,SListMap<IViewHandler> dFileList) {
		//下载文件URL
		String       url     = FILE_GUARD_ACTION+"?download=1&id=";
		InputStream  is      = null;             //文件读入流
		List<String> keyList = dFileList.keys(); //获取文件主键序列
		FileVO       fVO;                        //本地文件对象
		boolean      isNew;                      //是否为新文件
		IViewHandler vh;                         //返回的文件信息
		long         time;                       //文件更新时间
		File         file;                       //操作的文件
		String       allPath;                    //文件全路径
		//先处理新增的文件夹
		for(String id:keyList) {
			vh    = dFileList.get(id);
			if(!SBoolean.valueOf(vh.a("dir"))) {
				continue;
			}
			fVO   = fileMap.get(id);
			if(fVO!=null) {
				continue;
			}
			fVO          = new FileVO();
			fVO.id       = id;
			fVO.basePath = vh.a("base_path");
			fVO.path     = vh.a("path");
			fVO.time     = SLong.valueOf(vh.a("time"));
			fVO.md5      = vh.a("md5");
			fVO.isDir    = true;
			//创建文件夹
			file = SFilesUtil.createFile(filesUtil.getAllFilePath(fVO.basePath+"/"+fVO.path));
			file.setLastModified(fVO.time);
			//指定集群分组中的文件信息序列
			List<FileVO> fileList = fileListMap.get(groupName);
			if(fileList==null) {
				fileList = new ArrayList<FileVO>();
				fileListMap.put(groupName,fileList);
			}
			fileList.add(fVO);
			fileMap.put(id,fVO);
		}
		for(String id:keyList) {
			vh    = dFileList.get(id);
			if(SBoolean.valueOf(vh.a("dir"))) {
				//新的文件夹，已经处理过了
				continue;
			}
			fVO   = fileMap.get(id);
			time  = SLong.valueOf(vh.a("time"));
			if(fVO==null) {
				isNew        = true;
				fVO          = new FileVO();
				fVO.id       = id;
				fVO.basePath = vh.a("base_path");
        fVO.path     = vh.a("path");
        if(checkExclude(fVO.path,config.excludeMap.get(fVO.path))){
          fVO.ignore = true; //忽略该文件不做下载
          //指定集群分组中的文件信息序列
					List<FileVO> fileList = fileListMap.get(groupName);
					if(fileList==null) {
						fileList = new ArrayList<FileVO>();
						fileListMap.put(groupName,fileList);
					}
					fileList.add(fVO);
          fileMap.put(id,fVO);
          return;
        }
			}else {
        if(fVO.ignore){
          //忽略该文件不做更新
          continue;
        }
				isNew = false;
			}
			allPath = filesUtil.getAllFilePath(fVO.basePath);
			try {
				is = cf.download(serverName,url+id,null);
				if(is==null) {
					continue;
				}
				//累计接收文件次数
				if(++resFileCount>=Long.MAX_VALUE) {resFileCount=0;}
				file = SFilesUtil.createPath(fVO.path,allPath,true);
				FileCopyTools.copy(is,file); //执行覆盖
				file.setLastModified(time);  //设置最后更新时间

				fVO.time   = time;
				fVO.length = file.length();
				fVO.md5    = MD5.md5(file);
				if(isNew) {
					fVO.needReboot = config.needReboot(file);
					//指定集群分组中的文件信息序列
					List<FileVO> fileList = fileListMap.get(groupName);
					if(fileList==null) {
						fileList = new ArrayList<FileVO>();
						fileListMap.put(groupName,fileList);
					}
					fileList.add(fVO);
					fileMap.put(id,fVO);
					
					log(LOG_KEY,"[remote] [new] group:["+groupName+"] server:["+serverName+"] id:["+fVO.id+"] ["+fVO.md5+"] path:["+fVO.path+"] hash:["+fVO.hashCode()+"]");
				}else {
					log(LOG_KEY,"[remote] [changed] group:["+groupName+"] server:["+serverName+"] id:["+fVO.id+"] ["+fVO.md5+"] path:["+fVO.path+"] hash:["+fVO.hashCode()+"]");
				}
				if(fVO.needReboot) {
					warning("===从服务器:["+serverName+"]更新了文件：["+fVO.path+"] 需要重启服务，稍后进行重启。");
					rebootThread.ready(); //准备一分钟后重启
				}
			}catch(Exception e) {
				e.printStackTrace();
			}finally {
				try {
					is.close();
				}catch(Exception e) {}
				is = null;
			}
		}
	}
	
	/**
	 * 是否准备重启
	 * @return 是否准备重启
	 * 2018年7月31日
	 * @author MBG
	 */
	public boolean readyReboot() {
		return rebootThread != null && rebootThread.outTime > 0;
	}
	
	/**
	 * 返回管理文件数量
	 * @return 管理文件数量
	 * 2018年7月27日
	 * @author MBG
	 */
	public int fileCount() {
		if(fileMap==null) {
			return 0;
		}
		return fileMap.size();
  }
  
  
	/**
	 * 集群管理类初始化后调用改方法
	 * @param cf 集群管理类
	 * 2017年3月29日
	 * @author MBG
	 */
	public void clusterInited(ClusterFilter cf){
	  // 集群服务
      this.cf = cf;
      // 初始化配置信息
      config.init(px("file_guard"), cf.name(), cf.group());
      if (config.disasbled) {
        log("-------------FileGuard has Disabled-------------");
        return;
      }
      // 获取日志影子类
      XLogFilter xf = bean(XLogFilter.class);
      if (xf != null) {
        xf.addCustomLogFileKeys(LOG_KEY);
      }
      if (config.isShareMode || config.isMasterServer) {
        // 加载本地作业文件信息
        lfThread = new LocalFileMonitorThread();
        lfThread.start();
      }
      // 构建延时重启线程
      rebootThread = new RebootThread();
      rebootThread.start();
      log("\n******************************************\n"
          + "       ** FileGuard Enabled **\n\n******************************************\n\n");
      log(LOG_KEY, "FileGuard Started");
  }
	
	
	/**
	 * 首次完成发送心跳到某个目标服务器后执行该方法
	 * @param siVO   目标服务器信息类
	 * 2019年10月8日
	 * @author MBG
	 */
	public void validSended(ServerInfoVO siVO){
        // 发送完整数据
        IViewHandler data = getFileInfo(cf.getLocalServerInfoVO().group, false);
        // 执行发送
        try {
          cf.callAction(siVO.name, FILE_GUARD_ACTION + "?sync=1", data);
        } catch (Exception e) {
          e.printStackTrace();
        }
  }
	
	
	/**
	 * 首次完成接收目标心跳后执行该方法【无用】
	 * @param siVO   发送方服务器信息类
	 * 2019年10月8日
	 * @author MBG
	 */
	public void validReceived(ServerInfoVO siVO){}
	
	/**
	 * 返回触发响应主键
	 * @return 触发响应主键
	 * 2019年10月8日
	 * @author MBG
	 */
	public String triggerKey(){
    return "fileguard";
  }

	/**
	 * 响应发起服务器请求【无用】
	 * @param siVO   发起服务器信息类
	 * @param req    请求对象
	 * @param resp   反馈对象
	 * 2019年10月8日
	 * @author MBG
	 */
	public void triggerRecive(ServerInfoVO siVO,IRequest req,IResponse resp){}
}
